package pho.prowon;

import java.io.File;
import java.io.FileNotFoundException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import pho.helper.IptcTagNameConverter;

import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.Tag;
import com.drew.metadata.exif.ExifDirectory;
import com.drew.metadata.iptc.IptcDirectory;

//@PHO Date and time values should internally be kept in a invariable format, the property-based formats should be for get/set only

/**
 * @author Peter
 * @version 1.0
 *
 * This is an abstraction of metadata associated with Images. It encapsulates the metadata
 * read from the JpegMetadataReader and presents it. It can either be initialized from
 * an image file or can be created passing a file name and setting all available metadata
 * tags using setTag (if the metadata is retrieved from an index instead of the file directly)
 * 
 * How values are stored internally: (this can be configured through the properties prowon.DateFormat and prowon.TimeFormat
 * Date values: currently yyyy-MM-dd
 * Time values: currently HH:mm:ss
 * 
 * Values should also be set as strings in this format
 */

public class ImageData 
{
	private static final String DATE_FORMAT_PROPERTY = "prowon.DateFormat";
	private static final String TIME_FORMAT_PROPERTY = "prowon.TimeFormat";
	
	private String pathName;

	private Directory exifData;
	// imageData always contains String [] as values
	private Map imageData = new HashMap();
	private Collection imageDataList = new ArrayList ();
	
	private boolean hasChanged = false;
	private boolean valid = true;

	/**
	 * construct an ImageData object by passing a file name. The file is physically loaded
	 * to retrieve the metadata
	 * @param path the file path of the image file to be loaded
	 * 
	 */ 
	public ImageData (String path)
	{
		this (path, Boolean.TRUE);
	}

    /**
     * construct an ImageData object by passing a file name. the second parameter 
     * determines whether the file is physically loaded to initialize the ImageData
     * on construction or not
     * @param path the file path of the image file to be loaded
     * @param loadFile indicates whether the image data should be initialized by loading the file on construction
     * 
     */ 
	public ImageData (String path, Boolean loadFile)
	{
		if (path != null)
		{
			pathName = path;
			if (loadFile.booleanValue())
			{
				File jpegFile = new File (path);
				readMetaData (jpegFile);
			}
		}
		else
		{
			valid = false;
		}
	}
	
    /**
     * construct an ImageData object by passing a file object. the file is
     * physically loaded to initialize the ImageData on construction 
     * @param jpegFile the file object of the image file to be loaded
     */ 
	public ImageData (File jpegFile)
	{
		pathName = jpegFile.getAbsolutePath();
		readMetaData (jpegFile);
	}


    /**
     * use this method to refresh the metadata stored in the ImageData object
     * from the underlying physical file information (Iptc,...) any changes in the
     * internally stored metadata gets lost and the object will afterwards reflect
     * the state as stored on the file
     * @return true if the file could be read and was valid jpeg
     */ 
	public boolean refreshMetaData ()
	{		
		File jpegFile = new File (getPathName());
		// valid is set in readMetaData
		return readMetaData (jpegFile);
	}

    /**
     * read the metadata from the passed jpeg file and initialize the ImageData
     * sets the valid flag according to the result of the read process (i.e.
     * if the file was a valid Jpeg File the flag is set to true, in any other case
     * it is set to false) this does not indicate whether or not metadata information
     * was found on the file.
     * @param jpegFile the file object of the image file to be loaded
     * @return true if the file could be read and was valid jpeg
     */ 
	private boolean readMetaData (File jpegFile)
	{
		imageData.clear();
		imageDataList.clear();
		Metadata metadata = null;
		valid = false;

		try 
		{    	
			metadata = JpegMetadataReader.readMetadata(jpegFile);
		}
		catch (FileNotFoundException e)
		{
			// this cannot happen because FileNotFoundException is converted
			// to a JpegProcessingException in Metadata framework, declaration
			// in readMetadata and subsequently is wrong
			//@PHO Correct wrong throws declaration in readMetadata and subsequent, can never throw FileNotFoundException
			System.out.println (e.getMessage());
			return false;
		}				
		catch (JpegProcessingException e)
		{
			System.out.println (e.getMessage());
			return false;
		}
		catch (RuntimeException e)
		{
			// in case the Jpeg File is very invalid a NegativeArraySizeException
			// or the like may be thrown
			System.out.println (e.getMessage());
			return false;			
		}
		
		// now get the Iptc Directory, this could throw a RuntimeException
		// if the class cannot be instantiated, but we assume that something
		// is severly wrong if this happens and let the program crash
	    Directory directory = metadata.getDirectory(IptcDirectory.class);
	    
	    // iterate through tags and print to System.out
	    Iterator tags = directory.getTagIterator();
	    while (tags.hasNext()) 
	    {			    	
	    	Tag tag = (Tag)tags.next();
	    	
//@PHO Fix bug in metadata.directory that getStringArray can return a single integer as Stringarray
//@PHO Fix bug in metadata.directory that getStringArray can return a date as Stringarray
//@PHO Fix bug in metadata that BYTE Value of DirectoryVersion with length 1 is returned correctly
	    	
	    	String [] values = null;
			try
			{
				// at the moment we have to manually convert the integer value DirectoryVersion
				if (tag.getTagType() == IptcDirectory.TAG_RECORD_VERSION)
				{
					int dv = directory.getInt(tag.getTagType());
					values = new String [1];
					values [0] = Integer.toString (dv);
				}
				// at the moment we have to manually convert the date value DateCreated
				else if (tag.getTagType() == IptcDirectory.TAG_DATE_CREATED)
				{
					Date dv = directory.getDate(tag.getTagType());
					values = new String [1];					
					values [0] = dateToString(dv);
				}
				// at the moment we have to manually convert the time value TimeCreated
				else if (tag.getTagType() == IptcDirectory.TAG_TIME_CREATED)
				{
					String tv = directory.getString(tag.getTagType());
					values = new String [1];					
					values [0] = timeToString(tv);
				}
				else
				{
					values = directory.getStringArray(tag.getTagType());
				}
			}
			catch (MetadataException e1)
			{
				// we more or less ignore this because it is
				// unlikely to happen and if it happens this
				// tag is not relevant for processing --> so we
				// continue with the other tags
				System.out.println (e1.getMessage());
			}
			
			// set the tag only if there was some valid value
	    	if (values != null)
	    	{
				setImageDataTagArray(IptcTagNameConverter.getImageDataTagName (tag),values);
	    	}
	    }
	    
	    exifData = metadata.getDirectory(ExifDirectory.class);

		if (getImageDataTag("DateCreated").length() == 0)
		{
			fetchDateFromExif();
		}
		setInitialized();
		valid = true;
		return true;
	}

	/**
	 * Sets the value of an ImageData Tag with the name key to value
	 * if a value for that key already exists it will be overridden with the new value
	 * internally value will be filled into a String [] to be stored accordingly
	 * @param key the name of the image data tag to be set
	 * @param value the value of the new image data tag
	 */
	public void setImageDataTag (String key, String value)
	{
		if (value == null || value.trim().length() == 0)
		{
			if (getImageDataTag(key).length() != 0)
			{
				hasChanged = true;
			}
			removeObject(key);
		}
		else
		{
			if (!key.equals ("OriginatingProgram") && (isImageDataTagArray(key) || !getImageDataTag(key).equals(value)))
			{
				hasChanged = true;
			}
			String [] values = new String [1];
			values [0] = value;
			addObject (key, values);
		}
	}

	/**
	 * Sets the value of an ImageData Tag with the name key to array value
	 * if a value for that key already exists it will be overridden with the new value
	 * @param key the name of the image data tag to be set
	 * @param value the value of the new image data tag
	 */
	public void setImageDataTagArray (String key, String [] values)
	{
		if (key != null && !key.equals(""))
		{
			ArrayList newValues = new ArrayList();
			for (int i=0;i<values.length;i++)
			{
				if (values[i] != null && values [i].trim().length () != 0)
				{
					newValues.add(values [i]);
				}
			}
			
			if (newValues.size() > 0)
			{ 
				List oldValues = Arrays.asList(getImageDataTagArray(key));
				if (newValues.size() != oldValues.size() || !newValues.containsAll(oldValues))
				{
					hasChanged = true;
				}
				String [] newStringValues = new String [newValues.size()];
				newValues.toArray (newStringValues);
				addObject (key, newStringValues);
			}
			else
			{
				if (getImageDataTag(key).length ()!= 0)
				{
					hasChanged = true;
				}
				removeObject(key);
			}
		}
	}

	/**
	 * Returns true if the value of the image data Tag idTag is an array with more than
	 * one entry. Internally it is always stored as array so this only really tells
	 * about the number of entries
	 * @param idTag the name of the image data tag to be retrieved
	 * @return the value of the image data tag
	 */
	public boolean isImageDataTagArray (String idTag)
	{
		if (imageData.get(idTag) != null)
		{
			if (imageData.get(idTag).getClass().isArray())
			{
				if (((String []) imageData.get(idTag)).length > 1)
				{
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Returns the value of the image data Tag idTag
	 * this will return "" (empty string) in case the value does not exist or is
	 * null
	 * @param idTag the name of the image data tag to be retrieved
	 * @return the value of the image data tag
	 */
	public String getImageDataTag (String idTag)
	{
		if (imageData.get(idTag) != null)
		{
			String [] values = (String []) imageData.get (idTag);
			StringBuffer sb = new StringBuffer ();
			
			int totalLength = 0;
			for (int i = 0; i < values.length;i++)
			{
				// we don't care for emtpy strings and null values as they 
				// should already be removed at set method
				sb.append(values[i]);
				totalLength += values [i].length();
				if (i < values.length-1)
				{
					sb.append (", ");
				}
			}
			// if this is the case no real entries were in the array
			if (totalLength == 0)
			{
				return ""; 
			}
			return sb.toString();
		}
		return "";		
	}

	/**
	 * Returns the value of the image data Tag idTag in an array
	 * emtpy strings and null values have been removed at set method already
	 * in case the value does not exist we return an empty string in the array.
	 * @param idTag the name of the image data tag to be retrieved
	 * @return the value of the image data tag
	 */
	public String [] getImageDataTagArray (String idTag)
	{
		String [] ret;
		if (imageData.get(idTag) != null)
		{
			return (String []) imageData.get(idTag);		
		}
		ret = new String [1];
		ret [0] = "";
		return ret;
	}

	/**
	 * Returns an iterator over all set image data tags in this ImageData object
	 * @return iterator
	 */
	public Iterator getTagNameIterator ()
	{
		return imageDataList.iterator();
	}

    private void addObject(String idTag, Object value)
    {
        if (value == null) 
        {
            throw new NullPointerException("cannot set a null object");
        }

		if (!IptcTagNameConverter.isKnownImageDataTagName(idTag).booleanValue())
		{
			// unknown idTag
			return;
		}
                      
        if (!imageData.containsKey(idTag)) 
        {
            imageDataList.add(idTag);
        }
        imageData.put(idTag, value);
    }

	private void removeObject(String idTag)
	{
		if (!IptcTagNameConverter.isKnownImageDataTagName(idTag).booleanValue())
		{
			// unknown idTag
			return;
		}
                      
		if (imageData.containsKey(idTag)) 
		{
			imageData.remove(idTag);
			imageDataList.remove(idTag);
		}
	}

	/**
	 * Returns the pathName.
	 * @return String
	 */
	public String getPathName() 
	{
		return pathName;
	}

	/**
	 * Sets the pathName.
	 * @param pathName The pathName to set
	 */
	public void setPathName(String pathName) 
	{
		this.pathName = pathName;
	}

	/**
	 * This method retrieves the date and time created values from the Exif information of this file and 
	 * set the according IPTC Tags if the EXIF values exist.
	 */
	public void fetchDateFromExif ()
	{
		try
		{
			if (exifData != null)
			{
				if (exifData.containsTag(ExifDirectory.TAG_DATETIME))
				{
					setImageDataTag("DateCreated",dateToString(exifData.getDate(ExifDirectory.TAG_DATETIME)));
					setImageDataTag("TimeCreated",timeToString(exifData.getDate(ExifDirectory.TAG_DATETIME)));
				}
			}
		}
		catch (MetadataException e)
		{
			System.out.println (e.getMessage());
		}
	}
	
	
	
	/**
	 * This method retrieves the date format that will be used for GUI representation 
	 * and internal storage from the system property prowon.DateFormat 
	 * @return the date format string, default is yyyy-MM-dd
	 */
	
	public static String getDateFormat ()
	{
		return System.getProperty(DATE_FORMAT_PROPERTY,"yyyy-MM-dd");
	}

	/**
	 * This method retrieves the time format that will be used for GUI representation 
	 * and internal storafe from the system property
	 * prowon.TimeFormat 
	 * @return the time format string, default is HH:mm:ss
	 */
	public static String getTimeFormat ()
	{
		return System.getProperty(TIME_FORMAT_PROPERTY,"HH:mm:ss");
	}
	
	/**
	 * Helper method to convert a date into the internally stored date format
	 * @param date the date to be converted  
	 * @return the converted date as string
	 */
	private static String dateToString (Date date)
	{		
		DateFormat df = new SimpleDateFormat(getDateFormat());
		String value = df.format(date);
		return value;		
	}

	/**
	 * Helper method to convert a time string in HHmmss (format as stored in the IPTC tag in file)
	 * into the internally stored time format
	 * @param date the time string to be converted  
	 * @return the converted time as string
	 */
	private static String timeToString (String date)
	{		
		DateFormat df = new SimpleDateFormat("HHmmss");
		Date dv = new Date ();
		try
		{
			dv = df.parse(date);
		}
		catch (ParseException e)
		{
			System.out.println (e.getMessage());
			return "";
		}		
		return timeToString(dv);
	}

	/**
	 * Helper method to convert a date into the internally stored time format
	 * @param date the date to be converted  
	 * @return the converted time as string
	 */
	private static String timeToString (Date date)
	{		
		DateFormat df = new SimpleDateFormat(getTimeFormat());
		String value = df.format(date);
		return value;		
	}
	
	/**
	 * Indicates if this imagedata has been changed since its creation and initialization
	 * @return true if it has been changed
	 */
	public boolean hasChanged ()
	{
		return hasChanged;
	}
	
	/**
	 * This needs to be called if an image data is not loaded from the jpeg
	 * but initialized tag by tag through the index
	 */
	public void setInitialized ()
	{
		hasChanged = false;
	}
	
	/**
	 * Indicates if this imagedata could be loaded successfully from a valid Jpeg File
	 * this also returns true if the data was not initialized from a file at all (i.e. initialized
	 * from the index)
	 * @return true if it was loaded successfully
	 */
	public boolean isValid ()
	{
		return valid;
	}
}
